<template>
  <div class="message-toolbar">
    <tool-bar
      @insertEmotion="insertEmotion"
      @resetCursor="triggerResetCursor"
      :edit-ele="editMessage"
    ></tool-bar>
  </div>
  <div class="message-edit">
    <div
      spellcheck="false"
      ref="editMessage"
      class="message-edit__content"
      contenteditable="true"
      @keypress.enter.prevent="startSendMess(message)"
      @input="handleToInput"
      @compositionstart="compositStart"
      @compositionend="compositEnd"
      @paste.prevent="paste"
      @mouseup="updateCursor"
      @keyup="isUpdateCursor"
      @keyup.ctrl="isSelectAll"
      @drop.prevent="drop"
      @dragenter.prevent="triggerResetCursor"
    ></div>
  </div>
  <Modal v-model="showModal">
    <img class="img-browser" :src="imgSrc" alt="" />
  </Modal>
</template>

<script lang="ts" setup>
import { ref, reactive, nextTick, watch, onMounted } from "vue";
import toolBar from "./toolbar.vue";
import { ElMessage } from "element-plus";
import { onBeforeRouteUpdate } from "vue-router";
import { useSocketPinia } from "@/pinia/socket";
import Cookie from "@/plugins/cookie";
import { blobToBase64 } from "@/plugins/pictureProcess";
import Modal from "@/components/ui-components/modal.vue";
import { compress } from "image-conversion";

const props = withDefaults(
  defineProps<{
    modelValue?: string; // 用户编辑的内容值
    maxWordCount?: number; // 编辑的字数
    maxFilerCount?: number; // 文件的最大个数
    selfId: string; // 发送消息者的账号
    otherId: string; // 接受消息者的账号
  }>(),
  {
    modelValue: "",
    maxWordCount: 400,
    maxFilerCount: 9,
  }
);
const emits = defineEmits<{
  (e: "update:modelValue", message: string): void;
  (e: "sendMessage", message: string): void; // 开始发送消息
  (e: "disabled", boo: boolean): void; // 是否禁止发送消息(往往是用户未输入信息时触发)
}>();
const socketStore = useSocketPinia();

interface Cursor {
  startOffset: number;
  startContainer: Node | null;
  endOffset: number;
  endContainer: Node | null;
}
/**
 * 当前光标位置对象信息
 */
let cursorObj = reactive<Cursor>({
  startOffset: 0,
  startContainer: null,
  endOffset: 0,
  endContainer: null,
});

// 更新光标的位置
const updateCursor: () => void = (): void => {
  nextTick(() => {
    setTimeout(() => {
      const seleRange: Range = window.getSelection()!.getRangeAt(0);
      cursorObj.startContainer = seleRange.startContainer;
      cursorObj.startOffset = seleRange.startOffset;
      cursorObj.endContainer = seleRange.endContainer;
      cursorObj.endOffset = seleRange.endOffset;
    }, 0);
  });
};

// 用户是否按下Ctrl + a进行去全选
const isSelectAll: (e: KeyboardEvent) => void = (e: KeyboardEvent): void => {
  const key: string = e.key.toLowerCase();
  if (key === "a") updateCursor();
};

// 是否需要更新光标的位置
const isUpdateCursor: (e: KeyboardEvent) => void = (e: KeyboardEvent): void => {
  const key: string = e.key.toLowerCase();
  if (
    key === "arrowleft" ||
    key === "arrowright" ||
    key === "arrowup" ||
    key === "arrowdown"
  )
    updateCursor();
};

/**
 * 给编辑区域元素一个$ref属性
 */
let editMessage = ref<HTMLDivElement>();

// 发完消息后重新聚焦
const afterSendFocus = () => {
  const selection: Selection = window.getSelection()!;
  selection.selectAllChildren(editMessage.value!);
  selection.collapseToEnd();
  updateCursor();
};

defineExpose({
  afterSendFocus,
});

// 每当路由动态切换时输入框自动聚焦
onBeforeRouteUpdate(() => afterSendFocus());

/**
 * 当前用户编辑的内容
 */
let message = ref<string>(props.modelValue);

/**
 * 重置光标的位置
 */
const resetCursor: (
  startNode: Node,
  endNode: Node
) => (startOffset: number, endOffset: number) => void = (
  startNode: Node,
  endNode: Node
): ((startOffset: number, endOffset: number) => void) => {
  const selection: Selection = window.getSelection()!;
  selection.removeAllRanges();
  return (startOffset: number, endOffset: number): void => {
    try {
      const range: Range = new Range();
      range.setStart(startNode, startOffset); // 设置开始选取
      range.setEnd(endNode, endOffset); // 设置结束选取
      selection.addRange(range);
    } catch (error) {
      console.log(error);
    }
  };
};

// 触发重置光标函数
const triggerResetCursor: () => void = (): void =>
  resetCursor(cursorObj.startContainer!, cursorObj.endContainer!)(
    cursorObj.startOffset,
    cursorObj.endOffset
  );

/**
 * 设置输入框的值
 */
const setValue: (mess: string) => void = (mess: string): void => {
  const selection: Selection = window.getSelection()!;
  editMessage.value!.innerHTML = mess;
  if (mess === "") editMessage.value?.focus();
  else {
    // 将光标定在末尾
    const lastNode: ChildNode = editMessage.value?.childNodes[
      editMessage.value.childNodes.length - 1
    ]!;
    if (lastNode.nodeType !== 3) {
      selection.selectAllChildren(editMessage.value!);
      selection.collapseToEnd();
    } else {
      const len: number = lastNode.nodeValue?.length!;
      resetCursor(lastNode, lastNode)(len, len);
    }
  }
};

watch(
  (): string => props.modelValue,
  (newValue: string) => {
    message.value = newValue;
    if (newValue !== editMessage.value?.innerHTML) setValue(newValue);
  }
);

onMounted(() => {
  setValue(props.modelValue);
  nextTick(() => updateCursor());
});

/**
 * 文本段落的组成是否完成或取消时
 */
let isCompositEnd = ref<boolean>(true);

// 开始输入中文
const compositStart = () => {
  isCompositEnd.value = false;
};
// 输入中文结束
const compositEnd = (e: Event) => {
  isCompositEnd.value = true;
  handleToInput(e);
};

interface IsMaxCount {
  isMax: boolean;
  count: number;
}
/**
 * 判断此时输入框的字数是否超标
 */
const isMax: () => IsMaxCount = (): IsMaxCount => {
  let wordcount: number = editMessage.value?.innerText.length!; // 此时输入框的字数
  const imgs: Array<HTMLImageElement> = Array.from(
    editMessage.value!.querySelectorAll("img")
  );
  for (let item of imgs) if (item.dataset.key === "表情") wordcount += 1;
  if (wordcount < props.maxWordCount)
    return {
      isMax: false,
      count: wordcount,
    };
  return {
    isMax: true,
    count: wordcount,
  };
};

// 取消定时器(取消发送告诉接受信息者我不在输入当中)
const cancelTiming = ref();
const isSend = ref<boolean>(false);
// 告诉对方我是否正在输入中
const isTellIsInputing = (isInputing: boolean) => {
  const token: string = Cookie.getCookie("token");
  socketStore.SOCKET_SEND({
    url: "/talking/isInputing",
    data: JSON.stringify({
      isInputing,
      otherId: props.otherId,
      selfId: props.selfId,
    }),
    headers: {
      token,
    },
  });
};
/**
 * 用户输入时触发
 */
const handleToInput: (e: Event) => void = (e: Event): void => {
  const ele: HTMLDivElement = e.target as HTMLDivElement;
  if (isCompositEnd.value) {
    const selection: Selection = window.getSelection()!;
    const seleRange: Range = selection.getRangeAt(0);
    const ismax: IsMaxCount = isMax();
    const divEle: HTMLDivElement = document.createElement("div");
    divEle.innerHTML = message.value;
    // 内容变化前的值的长度
    let divEleTextLen: number = divEle.innerText.length;
    const imgs: Array<HTMLImageElement> = Array.from(divEle.querySelectorAll("img"));
    for (let item of imgs) if (item.dataset.key === "表情") divEleTextLen += 1;
    // 如果长度已经是最大长度且还增加内容
    if (divEleTextLen === props.maxWordCount && ismax.count > divEleTextLen) {
      cursorObj.startContainer = selection.anchorNode;
      cursorObj.startOffset = selection.anchorOffset;
      // 当前内容值的长度与原先内容值的长度差
      const differLen: number = ismax.count - divEleTextLen;
      const offset: number = cursorObj.startOffset - differLen;
      // 如果开始锚点是文本节点的话则其代表的是该节点的索引值
      const textNodeIndexStart: number = Array.from(
        editMessage.value?.childNodes!
      ).findIndex((item) => item === cursorObj.startContainer);
      ele.innerHTML = divEle.innerHTML;
      const childNodes: NodeListOf<ChildNode> = editMessage.value!.childNodes;
      const childNodesLen: number = childNodes.length;
      if (textNodeIndexStart >= childNodesLen)
        resetCursor(editMessage.value!, editMessage.value!)(childNodesLen, childNodesLen);
      else
        resetCursor(childNodes[textNodeIndexStart], childNodes[textNodeIndexStart])(
          offset,
          offset
        );
      ElMessage({
        message: `字数不能超过${props.maxWordCount}个`,
        type: "warning",
      });
      return;
    }
    if (ismax.isMax) {
      const currentNode: Node = selection.anchorNode!;
      const maxLen: number =
        props.maxWordCount + currentNode.nodeValue?.length! - ismax.count;
      const textNode: Text = document.createTextNode(
        currentNode.nodeValue?.slice(0, maxLen)!
      );
      seleRange.selectNode(currentNode);
      seleRange.deleteContents();
      seleRange.insertNode(textNode);
      seleRange.collapse(false);
    }
    emits("update:modelValue", ele.innerHTML);
    emits("disabled", !/[^&nbsp; ]/.test(ele.innerHTML));
    updateCursor();
  }
  if (!isSend.value) {
    if (cancelTiming.value) clearTimeout(cancelTiming.value);
    isSend.value = true;
    setTimeout(() => {
      isTellIsInputing(true);
      isSend.value = false;
      cancelTiming.value = setTimeout(() => isTellIsInputing(false), 2000);
    }, 1500);
  }
};

/**
 * 移除目标元素中的style属性
 * @param ele - 元素集合
 * @return 返回最底层的元素列表
 */
const deleSty: (ele: NodeListOf<ChildNode>) => Array<Element> = ((): ((
  ele: NodeListOf<ChildNode>
) => Array<Element>) => {
  let eleArr: Array<Element> = [];
  return (ele: NodeListOf<ChildNode>): Array<Element> => {
    Array.from(ele).forEach((item: any) => {
      if (item.childNodes?.length && item?.innerHTML) return deleSty(item.childNodes);
      else {
        // 如果是文本节点
        if (item.nodeType === 3 && item.nodeValue) {
          const spanEle = document.createElement("span");
          spanEle.innerText = item.nodeValue;
          eleArr.unshift(spanEle);
        } else if (item.nodeType === 1) {
          item?.removeAttribute("style");
          item?.removeAttribute("class");
          item?.removeAttribute("width");
          item?.removeAttribute("height");
          // 如果所粘贴的内容包含该表情的话
          if ((item as HTMLElement).dataset.key === "表情")
            item.className = "chat-edit__emotion";
          else if ((item as HTMLElement).dataset.key === "图片$")
            (item as HTMLElement).dataset.key = "图片";
          else (item as HTMLElement).removeAttribute("data-key");
          eleArr.unshift(item);
        }
      }
    });
    return eleArr;
  };
})();

// 检查编辑区的文件个数
const checkFile: (target: HTMLElement) => number = (target: HTMLElement): number => {
  let fileCount: number = 0;
  const imgs: Array<HTMLImageElement> = Array.from(target.querySelectorAll("img"));
  for (let item of imgs) if (item.dataset.key !== "表情") fileCount += 1;
  return fileCount;
};

/**
 * 当前用户查看的图片的url
 */
const imgSrc = ref<string>("");
/**
 * 是否显示模态框
 */
const showModal = ref<boolean>(false);
// 浏览图片时打开模态框
const openModal: (url: string) => void = (url: string): void => {
  imgSrc.value = url;
  showModal.value = true;
};

/**
 * 用户粘操作或者拖拽操作导致编辑区内容的变化
 * @param text - 用户所拖拽或者粘贴的内容
 */
const updateContent: (text: string) => void = (text: string) => {
  const selection: Selection = window.getSelection()!;
  const seleRange: Range = selection.getRangeAt(0);
  if (selection.rangeCount) {
    const divEle: HTMLDivElement = document.createElement("div");
    // 用户选中的内容
    const selectContent: DocumentFragment = seleRange.cloneContents();
    divEle.append(selectContent);
    // 匹配img标签
    const imgReg: RegExp = /<img.*?(?:>|\/>)/gi;
    // 用户选中的图片个数和字数
    let seleImgCount: number = 0,
      seleWordCount: number = divEle.innerText.length;
    if (imgReg.test(divEle.innerHTML)) {
      const imgs: Array<HTMLImageElement> = Array.from(divEle.querySelectorAll("img"));
      for (let item of imgs)
        if (item.dataset.key === "表情") seleWordCount += 1;
        else seleImgCount += 1;
    }
    // 清除divEle中的内容并且改为用户所粘贴的内容
    divEle.innerHTML = text;
    // 图片和文字数是否正确
    let isImgRight: boolean = true,
      isWordRight: boolean = true;
    // 判断编辑区的文件个数
    if (imgReg.test(divEle.innerHTML)) {
      let fileCount: number = checkFile(editMessage.value!);
      const imgsDivEle: Array<HTMLImageElement> = Array.from(
        divEle.querySelectorAll("img")
      );
      for (let item of imgsDivEle) if (item.dataset.key !== "表情") fileCount += 1;
      if (fileCount - seleImgCount > props.maxFilerCount) {
        isImgRight = false;
        ElMessage({
          message: `文件个数不能大于${props.maxFilerCount}个`,
          type: "warning",
        });
      }
    }
    // 粘贴内容的字数
    let divEleCount = divEle.innerText.length;
    const imgs: Array<HTMLImageElement> = Array.from(divEle.querySelectorAll("img"));
    for (let item of imgs) if (item.dataset.key === "表情") divEleCount += 1;
    // 判断编辑区的字数
    if (divEleCount + isMax().count - seleWordCount > props.maxWordCount) {
      isWordRight = false;
      ElMessage({
        message: `字数不能超过${props.maxWordCount}个`,
        type: "warning",
      });
    }
    // 如果图片个数和字数都正确
    if (isImgRight && isWordRight) {
      // 删除剪切板中的内容的样式
      let eleList: Array<Element> = deleSty(divEle.childNodes);
      let textNodeContent: string = "",
        eleListFormat: Array<Node> = [];
      eleList.forEach((item: Element) => {
        const innerText: string = (item as HTMLElement).innerText;
        if (item.nodeName.toLowerCase() === "img") {
          if ((item as HTMLElement).dataset.key !== "表情") {
            item.setAttribute("class", "edit-img");
            (item as HTMLElement).onclick = () =>
              openModal((item as HTMLImageElement).src);
          }
          if (textNodeContent) {
            const textNode: Text = document.createTextNode(textNodeContent);
            eleListFormat.push(textNode, item);
            textNodeContent = "";
          } else eleListFormat.push(item);
        } else if (innerText) textNodeContent = innerText + textNodeContent;
      });
      // 如果粘贴的内容全是文本无图片的话
      if (textNodeContent) {
        const textNode: Text = document.createTextNode(textNodeContent);
        eleListFormat.push(textNode);
      }

      if (!selection.isCollapsed) selection.deleteFromDocument();
      const eleFragment: DocumentFragment = document.createDocumentFragment();
      eleFragment.append(...eleListFormat.reverse());
      seleRange.insertNode(eleFragment);
      emits("update:modelValue", editMessage.value?.innerHTML!);
      emits("disabled", false);
      editMessage.value!.normalize();
      selection.collapseToEnd();
      updateCursor();
      eleList.splice(0);
    }
  }
};

// 压缩粘贴的文件图片
const compressImg = (file: Blob) => {
  return new Promise<string>(async (resolve) => {
    let blob: Blob = file;
    // 如果图片大于0.5MB的话则压缩图片
    if (blob.size / 1024 / 1024 > 0.5) blob = await compress(blob, 0.25);
    blobToBase64(blob, (url: string) => resolve(url));
  });
};

// 用户粘贴操作
const paste: (e: ClipboardEvent) => void = async (e: ClipboardEvent) => {
  try {
    const clipItems: ClipboardItems = await navigator.clipboard.read();
    for (const clipItem of clipItems) {
      if (clipItem.types.length === 1) {
        const blob: Blob = await clipItem.getType(clipItem.types[0]);
        if (blob.type.startsWith("text")) {
          const text = await blob.text();
          updateContent(text);
        } else if (blob.type.startsWith("image"))
          compressImg(blob).then((url) =>
            updateContent(`<img class="edit-img" src="${url}" data-key="图片$" />`)
          );
      } else
        for (const type of clipItem.types) {
          if (type === "text/html") {
            const blob: Blob = await clipItem.getType(type);
            if (blob.type.startsWith("text")) {
              const text = await blob.text();
              updateContent(text);
            } else if (blob.type.startsWith("image"))
              compressImg(blob).then((url) =>
                updateContent(`<img class="edit-img" src="${url}" data-key="图片$" />`)
              );
          }
        }
    }
  } catch (error: any) {
    console.log(error.name, error.message);
    const clipItemList = e.clipboardData?.items;
    if (clipItemList) {
      if (clipItemList.length === 1)
        if (clipItemList[0].kind === "file") {
          const file = clipItemList[0].getAsFile();
          if (file?.name.startsWith("image"))
            compressImg(file).then((url) =>
              updateContent(`<img class="edit-img" src="${url}" data-key="图片$" />`)
            );
        } else clipItemList[0].getAsString((str) => updateContent(str));
      else
        for (let i = 0, len = clipItemList.length; i < len; i++) {
          const item = clipItemList[i];
          if (item.kind === "string" && item.type === "text/html")
            item.getAsString((str) => updateContent(str));
          else if (item.kind === "file") {
            const file = clipItemList[0].getAsFile();
            if (file?.name.startsWith("image"))
              compressImg(file).then((url) =>
                updateContent(`<img class="edit-img" src="${url}" data-key="图片$" />`)
              );
          }
        }
    }
  }
};

// 用户拖拽内容到编辑区时执行的动作
const drop = (e: DragEvent) => {
  const dataTransfer: DataTransfer = e.dataTransfer!;
  if (dataTransfer.items !== undefined)
    for (let item of Array.from(dataTransfer.items))
      if (item.type.match("^text/html")) item.getAsString((s) => updateContent(s));
};

/**
 * 插入表情
 * @param target - 插入的元素
 * @param isInsertFile - 是插入图片还是插入表情
 */
const insertEmotion: (target: Array<Node>, isInsertFile: boolean) => void = (
  target: Array<Node>,
  isInsertFile: boolean
): void => {
  const selection: Selection = window.getSelection()!;
  const seleRange: Range = selection.getRangeAt(0);
  // 光标是否在编辑区内
  const isEditContain: boolean =
    seleRange.commonAncestorContainer === editMessage.value ||
    seleRange.commonAncestorContainer.parentNode === editMessage.value;
  if (isEditContain) {
    function insertContent(): void {
      const eleFragment: DocumentFragment = document.createDocumentFragment();
      target.forEach((item: any) => {
        if (item.dataset.key === "图片") item.onclick = () => openModal(item.src);
      });
      eleFragment.append(...target);
      seleRange.insertNode(eleFragment);
      selection.collapseToEnd();
      updateCursor();
      emits("update:modelValue", editMessage.value!.innerHTML);
      emits("disabled", false);
    }
    function errorWarn(message: string, isResetCursor: boolean): void {
      if (isResetCursor)
        resetCursor(cursorObj.startContainer!, cursorObj.endContainer!)(
          cursorObj.startOffset,
          cursorObj.endOffset
        );
      ElMessage({
        message,
        type: "warning",
      });
    }

    // 如果插入的是表情
    if (!isInsertFile) {
      if (!selection.isCollapsed) seleRange.deleteContents();
      if (!isMax().isMax) insertContent();
      else errorWarn(`字数不能超过${props.maxWordCount}个`, true);
    } else {
      const fileCount: number = checkFile(editMessage.value!);
      // 如果没有选区的话
      if (selection.isCollapsed) {
        if (fileCount + target.length <= props.maxFilerCount) insertContent();
        else errorWarn(`文件数不能超过${props.maxFilerCount}个`, false);
      } else {
        const divEle: HTMLDivElement = document.createElement("div");
        // 用户选中的内容
        const selectContent: DocumentFragment = seleRange.cloneContents();
        divEle.append(selectContent);
        // 匹配img标签
        const imgReg: RegExp = /<img.*?(?:>|\/>)/gi;
        // 用户选中的图片个数
        let seleImgCount: number = 0;
        if (imgReg.test(divEle.innerHTML)) {
          const imgs: Array<HTMLImageElement> = Array.from(
            divEle.querySelectorAll("img")
          );
          for (let item of imgs) if (item.dataset.key !== "表情") seleImgCount += 1;
        }
        if (fileCount - seleImgCount + target.length > props.maxFilerCount)
          ElMessage({
            message: `文件个数不能大于${props.maxFilerCount}个`,
            type: "warning",
          });
        else {
          seleRange.deleteContents();
          insertContent();
        }
      }
    }
  }
};

// 开始发送信息
const startSendMess = (content: string) => emits("sendMessage", content);
</script>

<style lang="scss" scoped>
.message {
  &-toolbar {
    @include centerVertically(45px);
    padding-left: 30px;
  }

  &-edit {
    padding: 0 15px 0 30px;
    position: relative;

    &__content {
      height: 45px;
      overflow: hidden auto;
      word-break: break-all;
      @include scrollBar;

      &:focus {
        outline: none;
      }
    }
  }
}
</style>
